﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Diagnostics;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Threading;

namespace Core
{
    class Worker
    {
        private NetworkStream ns;
        private SslStream sslStream = null;
        private bool ssl = false;
        string clientIp = string.Empty;

        TcpClient tcpSource;
        Socket socketDestination;

        private Utils utils;

        //public Stopwatch executionTime { get { return utils.executionTime; } }
        //public Stopwatch executionSend { get { return utils.executionSend; } }
        //public Stopwatch executionRead { get { return utils.executionRead; } }

        public Worker(TcpClient tcpClient)
        {
            
            utils = new Utils();
            //utils.executionTime.Start();

            this.tcpSource = tcpClient;
            clientIp = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address.ToString();
            this.ns = tcpClient.GetStream();
            this.ssl = false; // By default. It will be changed to true if necesary
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="timeout">Timeout 0 or -1 means no timeout.</param>
        public void Process()
        {
            /*
            executionTime.Start();

            if (timeout == 0 || timeout == -1)
                _Process();
            else
            {
                var thread = new Thread(_Process);
                thread.Start();
                var completed = thread.Join(timeout);
                if (!completed)
                    thread.Abort();
            }*/
            _Process();
        }

        private void _Process()
        {
            // To connect from the proxy to the remote server
            NetworkStream remoteNS = null;
            SslStream remoteSSLStream = null;

            // Buffered stream that allows to be restarted, essential to use http and https at the same port
            Auxiliar.RestartableReadStream dualStream = new Auxiliar.RestartableReadStream(ns);

            try
            {
                {
                    try
                    {
                        sslStream = new SslStream(dualStream);
                        
                        // This mode is for transparent connections (used as a reverse proxy), so just one certificated is configurable
                        sslStream.AuthenticateAsServer(new System.Security.Cryptography.X509Certificates.X509Certificate(Configuration.Configuration.reverseProxyCertFile, Configuration.Configuration.reverseProxyCertPassword));
                        if (!sslStream.IsAuthenticated)
                            return;
                        ssl = true;
                    }
                    catch
                    {
                        ssl = false;
                        dualStream.Restart();
                    }
                }


                // Gets the request from the client
                Parsers.Request requestParser = new Parsers.Request();

                if (!ssl)
                    requestParser.Parse(utils.ReadRequest(dualStream), false);
                else
                    requestParser.Parse(utils.ReadRequest(sslStream), true);

                if (requestParser == null || requestParser.error)
                    return;

                // This mode is for proxy-connection mode
                if (requestParser.GetPacket().method.ToLower() == "connect")
                {
                    string response = string.Concat(requestParser.GetPacket().version, " 200 Connection estabilished", "\r\n\r\n");
                    ns.Write(ASCIIEncoding.ASCII.GetBytes(response), 0, response.Length);
                    try
                    {
                        sslStream = new SslStream(dualStream);
                        string certPath = Certs.CertGenerator.GenerateCert(requestParser.remoteHost);
                        sslStream.AuthenticateAsServer(new System.Security.Cryptography.X509Certificates.X509Certificate(certPath, Configuration.Configuration.reverseProxyCertPassword));
                        if (!sslStream.IsAuthenticated)
                            return;
                        ssl = true;
                    }
                    catch
                    {
                        // What CONNECT is encapsulating isn't SSL
                        ssl = false;
                        dualStream.Restart();
                    }

                    byte[] requestConnect = null;

                    if (ssl == false)
                        requestConnect = utils.ReadRequest(dualStream);
                    else
                        requestConnect = utils.ReadRequest(sslStream);

                    if (requestConnect.Length == 0)
                        return;

                    Parsers.Request tmpRequestParser = new Parsers.Request();

                    if (!ssl)
                        tmpRequestParser.Parse(requestConnect, false);
                    else
                        tmpRequestParser.Parse(requestConnect, true);

                    tmpRequestParser.remoteHost = requestParser.remoteHost;
                    tmpRequestParser.remotePort = requestParser.remotePort;
                    requestParser = tmpRequestParser;
                }

                Model.Elements.HttpRequestPacket requestPacket = GetRequestPacketFromClient(requestParser);
                requestPacket.clientIp = this.clientIp;
                requestPacket = Plugins.PluginManager.ProcessPlugins(requestPacket as Model.Elements.HttpPacket) as Model.Elements.HttpRequestPacket;

                if (requestPacket == null)
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get400Error("Received an empty packet from the client"), dualStream);
                    else
                        utils.SendResponsePacket(utils.Get400Error("Received an empty packet from the client"), sslStream);
                    return;
                }

                if (requestPacket.GetforcedResponsePacket() != null)
                {
                    Model.Elements.HttpResponsePacket forcedResponseAfterPlugins = Plugins.PluginManager.ProcessPlugins(requestPacket.GetforcedResponsePacket() as Model.Elements.HttpPacket) as Model.Elements.HttpResponsePacket;

                    forcedResponseAfterPlugins.Compress();
                    // Happends when a plugin assign a response for this packet
                    if (!ssl)
                        utils.SendResponsePacket(forcedResponseAfterPlugins, dualStream);
                    else
                        utils.SendResponsePacket(forcedResponseAfterPlugins, sslStream);
                    return;

                }
                else if (requestPacket.dropped)
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get503Error("Packet dropped (request)"), dualStream);
                    else
                        utils.SendResponsePacket(utils.Get503Error("Packet dropped (request)"), sslStream);
                    return;
                }

                requestPacket.RemoveHeader("Host", false);



                // Gets the data where it has to redirect the traffic
                IPAddress ip = null;
                string hostname = string.Empty;
                int remotePort = 0;

                if (!GetFordwardData(requestParser, out ip, out hostname, out remotePort))
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get500Error("Error parsing forward data"), dualStream);
                    else
                        utils.SendResponsePacket(utils.Get500Error("Error parsing forward data"), sslStream);
                    return;
                }

                // It adds the proper Host header with the remote hostname
                requestPacket.AddHeader(new Model.Elements.HttpHeader("Host", hostname));

                // Connects to the remote server
                IPEndPoint ipep = new IPEndPoint(ip, remotePort);
                socketDestination = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                try
                {
                    socketDestination.Connect(ipep);
                }
                catch
                {
                    // Problem connecting. May be because of a IPv6 or whatever
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get500Error(string.Format("Error while connecting to {0}", ipep.ToString())), dualStream);
                    else
                        utils.SendResponsePacket(utils.Get500Error(string.Format("Error while connecting to {0}", ipep.ToString())), sslStream);
                    return;
                }
                remoteNS = new NetworkStream(socketDestination);

                if (ssl)
                {
                    remoteSSLStream = new SslStream(remoteNS);
                    remoteSSLStream.AuthenticateAsClient(requestParser.remoteHost);
                }

                if (ssl)
                    utils.SendRequestPacket(requestPacket, remoteSSLStream);
                else
                    utils.SendRequestPacket(requestPacket, remoteNS);

                Parsers.Response responseParser = new Parsers.Response();
                byte[] responseServer = null;
                if (ssl)
                    responseServer = utils.ReadResponse(remoteSSLStream);
                else
                    responseServer = utils.ReadResponse(remoteNS);

                responseParser.Parse(responseServer);

                if (responseParser.error)
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get500Error("Error while parsing response from server"), ns);
                    else
                        utils.SendResponsePacket(utils.Get500Error("Error while parsing response from server"), sslStream);
                    return; // Error while parsing the response of the server
                }

                Model.Elements.HttpResponsePacket responsePacket = responseParser.GetPacket();
                if (responsePacket == null)
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get500Error("Received an empty packet from the server"), ns);
                    else
                        utils.SendResponsePacket(utils.Get500Error("Received an empty packet from the server"), sslStream);
                    return;
                }
                responsePacket.SetRequestPacket(requestPacket);

                responsePacket = Plugins.PluginManager.ProcessPlugins(responsePacket as Model.Elements.HttpPacket) as Model.Elements.HttpResponsePacket;

                if (responsePacket.dropped)
                {
                    if (!ssl)
                        utils.SendResponsePacket(utils.Get503Error("Packet dropped (response)"), ns);
                    else
                        utils.SendResponsePacket(utils.Get503Error("Packet dropped (response)"), sslStream);
                    return;
                }

                responsePacket.Compress();

                if (!ssl)
                    utils.SendResponsePacket(responsePacket, ns);
                else
                    utils.SendResponsePacket(responsePacket, sslStream);

                if (requestPacket.ExistHeader("Proxy-Connection", true))
                {
                }
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception ex)
            {
                Model.Elements.HttpResponsePacket errorPacket = utils.Get500Error(string.Format("Unknow error: {0}", ex.Message));
                if (!ssl)
                    utils.SendResponsePacket(errorPacket, ns);
                else
                    utils.SendResponsePacket(errorPacket, sslStream);

                //Console.WriteLine(ex.Message);
            }
            finally
            {
                if (ssl && sslStream != null)
                    sslStream.Close();
                if (ns != null)
                    ns.Close();
                if (remoteNS != null)
                    remoteNS.Close();
                if (remoteSSLStream != null)
                    remoteSSLStream.Close();
                if (dualStream != null)
                    dualStream.Close();
            }
        }

        private Model.Elements.HttpRequestPacket GetRequestPacketFromClient(Parsers.Request requestParser)
        {
            if (requestParser.error)
                return null; // Error while parsing the request of the client

            Model.Elements.HttpRequestPacket requestPacket = requestParser.GetPacket();

            return requestPacket;
        }

        private bool GetFordwardData(Parsers.Request requestParser, out  IPAddress ip, out string hostname, out int port)
        {
            Configuration.RelationDomainIp relation = utils.GetFordwardOfHost(requestParser.remoteHost);
            //int remotePort = 0;
            //IPAddress ip = null;

            if (relation != null)
            {
                port = relation.remotePort;
                hostname = relation.remoteHostName;
                // IP is asigned down here
                if (!IPAddress.TryParse(relation.remoteIp, out ip))
                {
                    // Invalid IP address in the relation (configuration file!)
                    ip = null;
                    hostname = string.Empty;
                    port = 0;
                    return false; //
                }
            }
            else
            {
                // There is no relation... then its time to check if the proxy is configured as 'open', which means
                // that users can connect to any remote server using this proxy server.
                if (Configuration.Configuration.openProxy)
                {
                    port = requestParser.remotePort;
                    hostname = requestParser.remoteHost;
                    if (!IPAddress.TryParse(requestParser.remoteHost, out ip))
                    {
                        try
                        {
                            IPAddress[] lstIps = Dns.GetHostAddresses(requestParser.remoteHost);
                            if (lstIps.Length == 0)
                            {
                                ip = null;
                                hostname = string.Empty;
                                port = 0;
                                return false; // remote host doesnt have an ip address
                            }

                            //Only ipv4, not ipv6!
                            foreach (IPAddress ipAdd in lstIps)
                            {
                                if (ipAdd.AddressFamily ==  System.Net.Sockets.AddressFamily.InterNetworkV6)
                                    continue;

                                if (ssl)
                                    port = 443;
                                else
                                    port = 80;

                                ip = ipAdd;
                                break;
                            }

                            if (ip == null)
                            {
                               // No ipv4 found
                                ip = null;
                                hostname = string.Empty;
                                port = 0;
                                return false;
                            }
                        }
                        catch
                        {
                            // No such host is know. It doesnt have even an dns
                            ip = null;
                            hostname = string.Empty;
                            port = 0;
                            return false;
                        }
                    }
                    // Else { is a IP Address, so everything allright }

                }
                else
                {
                    ip = null;
                    hostname = string.Empty;
                    port = 0;
                    return false; // No relations and proxy not configured as open
                }
            }

            return true;
        }

        static bool CertificateValidationCallback(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }
    }
}
